import hivemind from 'hivemind';
import {getExitCenters} from 'utils/room-info';
import {getRoomIntel} from 'room-intel';

declare global {
	type VariationInfo = {
		roomCenter?: RoomPosition;
		sourcesWithSpawn?: Array<Id<Source>>;
	};
}

export default class VariationGenerator {
	protected variations: Record<string, VariationInfo>;
	protected variationKeys: string[];

	constructor(protected readonly roomName: string, protected wallDistanceMatrix: CostMatrix, protected exitDistanceMatrix: CostMatrix) {}

	generateVariations() {
		if (this.variations) return;

		this.variations = this.varyBy(null, variation => this.varyRoomCenter(variation));
		this.variations = this.varyBy(this.variations, variation => this.varySourceSpawns(variation));

		this.variationKeys = _.keys(this.variations);
	}

	varyBy(originalVariations: Record<string, VariationInfo>, callback: (variation: VariationInfo) => Record<string, VariationInfo>): Record<string, VariationInfo> {
		if (!originalVariations) return callback({});

		const variations = {};
		for (const key in originalVariations) {
			const modifiedVariations = callback(originalVariations[key]);

			for (const newSuffix in modifiedVariations) {
				variations[key + ':' + newSuffix] = modifiedVariations[newSuffix];
			}
		}

		return variations;
	}

	varyRoomCenter(baseVariation: VariationInfo): Record<string, VariationInfo> {
		const potentialCorePositions = this.collectPotentialCorePositions();

		const weightedCenterVariation = {
			...baseVariation,
			roomCenter: this.chooseCorePosition(potentialCorePositions),
		};

		if (!weightedCenterVariation.roomCenter) return {};

		const variations = {weighted: weightedCenterVariation};
		this.addGridCorePositions(variations, baseVariation, potentialCorePositions);

		return variations;
	}

	collectPotentialCorePositions(): RoomPosition[] {
		const terrain = new Room.Terrain(this.roomName);
		const potentialCorePositions: RoomPosition[] = [];

		for (let x = 0; x < 50; x++) {
			for (let y = 0; y < 50; y++) {
				if (terrain.get(x, y) === TERRAIN_MASK_WALL) continue;

				const wallDistance = this.wallDistanceMatrix.get(x, y);
				const exitDistance = this.exitDistanceMatrix.get(x, y);

				if (wallDistance >= 3 && wallDistance < 255 && exitDistance > 8) {
					potentialCorePositions.push(new RoomPosition(x, y, this.roomName));
				}
			}
		}

		return potentialCorePositions;
	}

	chooseCorePosition(potentialCorePositions: RoomPosition[]) {
		const roomIntel = getRoomIntel(this.roomName);
		const controllerPosition = roomIntel.getControllerPosition();

		if (!controllerPosition) return null;

		const exitCenters = getExitCenters(this.roomName);

		// Decide where room center should be by averaging exit positions.
		// @todo Try multiple room centers:
		// - Current version
		// - Near controller
		// - Between controller and a source
		// - Near any corner or side
		// @todo Then evaluate best result by:
		// - Upkeep costs (roads, ramparts)
		// - Path lengths (Bays, sources, controller)
		let cx = controllerPosition.x;
		let cy = controllerPosition.y;
		let count = 1;
		for (const dir of _.keys(exitCenters)) {
			for (const pos of exitCenters[dir]) {
				count++;
				cx += pos.x;
				cy += pos.y;
			}
		}

		// Also include source and mineral positions when determining room center.
		for (const mineral of roomIntel.getMineralPositions()) {
			count++;
			cx += mineral.x;
			cy += mineral.y;
		}

		for (const source of roomIntel.getSourcePositions()) {
			count++;
			cx += source.x;
			cy += source.y;
		}

		cx = Math.floor(cx / count);
		cy = Math.floor(cy / count);

		// Find closest position with distance from walls around there.
		const roomCenter = _.min(potentialCorePositions, p => p.getRangeTo(cx, cy));
		if (!roomCenter || (typeof roomCenter === 'number')) {
			hivemind.log('rooms', this.roomName).error('Could not find a suitable center position!');
			return null;
		}

		return roomCenter;
	}

	addGridCorePositions(variations: Record<string, VariationInfo>, baseVariation: VariationInfo, potentialCorePositions: RoomPosition[]) {
		const subdivisionCount = 3;
		const bestOptions: Record<string, {
			distance: number;
			position: RoomPosition;
		}> = {};

		for (const position of potentialCorePositions) {
			const subDivision = Math.floor(position.x * subdivisionCount / 50) + 'x' + Math.floor(position.y * subdivisionCount / 50);

			if (!bestOptions[subDivision] || bestOptions[subDivision].distance < this.wallDistanceMatrix.get(position.x, position.y)) {
				bestOptions[subDivision] = {
					distance: this.wallDistanceMatrix.get(position.x, position.y),
					position,
				};
			}
		}

		for (const subDivision in bestOptions) {
			variations[subDivision] = {
				...baseVariation,
				roomCenter: bestOptions[subDivision].position,
			};
		}
	}

	varySourceSpawns(baseVariation: VariationInfo): Record<string, VariationInfo> {
		const roomIntel = getRoomIntel(this.roomName);
		const sources = roomIntel.getSourcePositions();

		let currentVariations: Record<string, VariationInfo> = {
			'': {...baseVariation, sourcesWithSpawn: []},
		};

		for (const source of sources) {
			const modifiedVariations: Record<string, VariationInfo> = {};
			for (const key in currentVariations) {
				const withNewSource = [...currentVariations[key].sourcesWithSpawn];
				withNewSource.push(source.id);

				modifiedVariations[key + '-'] = {
					...currentVariations[key],
				};
				modifiedVariations[key + '+'] = {
					...currentVariations[key],
					sourcesWithSpawn: withNewSource,
				};
			}

			currentVariations = modifiedVariations;
		}

		return currentVariations;
	}

	getVariationList(): string[] {
		this.generateVariations();
		return this.variationKeys;
	}

	getVariationAmount(): number {
		this.generateVariations();
		return this.variationKeys.length;
	}

	getVariationInfo(key: string) {
		return this.variations[key];
	}
}
